﻿//AcademicPlanner - course registration planning web app.
//Copyright (C) 2009  Boubacar Diallo

//This program is free software; you can redistribute it and/or
//modify it under the terms of the GNU General Public License
//as published by the Free Software Foundation; either version 2
//of the License, or (at your option) any later version.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.

//You should have received a copy of the GNU General Public License
//along with this program; if not, write to the Free Software
//Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using AcademicPlanner.Domain.Models;
using AcademicPlanner.Domain.Models.Collections;
using AcademicPlanner.Framework.Helpers;

namespace AcademicPlanner.Domain.Components
{
	/// <summary>
	/// AcademicSchedulingMngr is the component of the application responsible
	/// for operations related to managing academic schedules.
	/// </summary>
	/// <since>1.0</since>
	public class AcademicSchedulingMngr : Component
	{
		AcademicSchedulingMngr() { }

		class InstanceHolder
		{
			internal static AcademicSchedulingMngr INSTANCE = new AcademicSchedulingMngr();
		}


		internal static AcademicSchedulingMngr GetInstance()
		{
			return InstanceHolder.INSTANCE;
		}

		/// <summary>
		/// Gets the <c>AcademicSchedule</c> with the given identifier in the system.
		/// </summary>
		/// <param name="pEntityId">The identifier.</param>
		/// <returns>The <c>AcademicSchedule</c> or <c>null</c>.</returns>
		/// <since>1.0</since>
		public AcademicSchedule GetAcademicSchedule(int pEntityId)
		{
			return DomainMaster.MainModel.AllSchedules.WithId(pEntityId);
		}

		/// <summary>
		/// Creates and saves a <c>AcademicSchedule</c>.
		/// </summary>
		/// <param name="pPlanId">
		/// The identifier of the <c>AcademicPlan</c> the <c>AcademicSchedule</c> will belong to.
		/// </param>
		/// <param name="pScheduleName">The name of the <c>AcademicSchedule</c>.</param>
		/// <param name="pScheduleProgramId">
		/// The identifier of the <c>Program</c> the <c>AcademicSchedule</c> is for.
		/// </param>
		/// <param name="pStartYear">The year the <c>AcademicSchedule</c> begins in.</param>
		/// <param name="pStartTermId">
		/// The identifier of the <c>AcademicTerm</c> the <c>AcademicSchedule</c> begins in.
		/// </param>
		/// <remarks>
		/// <c>NotCriticalDomainException</c>s may be indirectly thrown.
		/// <c>InvalidModelPropertiesException</c>s may be indirectly thrown.
		/// <c>NotAuthorizedException</c>s may be indirectly thrown in case of access control
		/// restrictions.
		/// </remarks>
		/// <exception cref="T:AcademicPlanner.Domain.NotCriticalDomainException">
		/// Thrown with error code <c>DomainErrorCode.C_ACADEMICPLAN_TO_SAVE_NEW_ACADEMICSCHEDULE_UNDER_NOT_FOUND</c>
		/// if the <c>AcademiPlan</c> with the given identifier does not exist.
		/// </exception>
		/// <seealso cref="P:DomainMaster"/>
		/// <seealso cref="P:DomainMaster.Planning"/>
		/// <seealso cref="T:AcademicPlanningMngr"/>
		/// <seealso cref="M:AcademicPlanningMngr.GetAcademicPlan(int)"/>
		/// <since>1.0</since>
		public void SaveNewAcademicSchedule(int pPlanId, string pScheduleName, int pScheduleProgramId,
												int? pStartYear, int? pStartTermId)
		{
			var vPlan = DomainMaster.Planning.GetAcademicPlan(pPlanId);
			if (vPlan == null)
				throw new NotCriticalDomainException(DomainErrorCode.C_ACADEMICPLAN_TO_SAVE_NEW_ACADEMICSCHEDULE_UNDER_NOT_FOUND);

			var vTask = new SaveNewAcademicScheduleTask(vPlan, pScheduleName, pScheduleProgramId,
														pStartYear, pStartTermId);
			vTask.Run();
		}

		/// <summary>
		/// Updates and saves a <c>AcademicSchedule</c>.
		/// </summary>
		/// <param name="pScheduleId">The identifier of the <c>AcademicSchedule</c>.</param>
		/// <param name="pScheduleName">The name of the <c>AcademicSchedule</c>.</param>
		/// <remarks>
		/// <c>NotCriticalDomainException</c>s may be indirectly thrown.
		/// <c>InvalidModelPropertiesException</c>s may be indirectly thrown.
		/// <c>NotAuthorizedException</c>s may be indirectly thrown in case of access control
		/// restrictions.
		/// </remarks>
		/// <exception cref="T:AcademicPlanner.Domain.NotCriticalDomainException">
		/// Thrown with error code <c>DomainErrorCode.C_ACADEMICSCHEDULE_TO_SAVE_NOT_FOUND</c>
		/// if the <c>AcademicSchedule</c> with the given identifier does not exist.
		/// </exception>
		/// <seealso cref="M:GetAcademicSchedule"/>
		/// <since>1.0</since>
		public void SaveAcademicSchedule(int pScheduleId, string pScheduleName)
		{
			var vSchedule = GetAcademicSchedule(pScheduleId);
			if (vSchedule == null)
				throw new NotCriticalDomainException(DomainErrorCode.C_ACADEMICSCHEDULE_TO_SAVE_NOT_FOUND);

			var vTask = new SaveAcademicScheduleTask(vSchedule, pScheduleName);
			vTask.Run();
		}

		/// <summary>
		/// Deletes a <c>AcademicSchedule</c>.
		/// </summary>
		/// <remarks>
		/// The <c>AcademicSchedule</c> is essentially trashed but not
		/// completely deleted from the storage.
		/// </remarks>
		/// <param name="pScheduleId">The identifier of the <c>AcademicSchedule</c>.</param>
		/// <remarks>
		/// <c>NotCriticalDomainException</c>s may be indirectly thrown.
		/// <c>InvalidModelPropertiesException</c>s may be indirectly thrown.
		/// <c>NotAuthorizedException</c>s may be indirectly thrown in case of access control
		/// restrictions.
		/// </remarks>
		/// <exception cref="T:AcademicPlanner.Domain.NotCriticalDomainException">
		/// Thrown with error code <c>DomainErrorCode.C_ACADEMICSCHEDULE_TO_DELETE_NOT_FOUND</c>
		/// if the <c>AcademicSchedule</c> with the given identifier does not exist.
		/// </exception>
		/// <seealso cref="M:GetAcademicSchedule"/>
		/// <since>1.0</since>
		public void DeleteAcademicSchedule(int pScheduleId)
		{
			var vSchedule = GetAcademicSchedule(pScheduleId);
			if (vSchedule == null)
				throw new NotCriticalDomainException(DomainErrorCode.C_ACADEMICSCHEDULE_TO_DELETE_NOT_FOUND);

			var vTask = new DeleteAcademicScheduleTask(vSchedule);
			vTask.Run();
		}

		/// <summary>
		/// Updates and saves the <c>Registrations</c> of a <c>AcademicSchedule</c>.
		/// </summary>
		/// <param name="pScheduleId">The identifier of the <c>AcademicSchedule</c>.</param>
		/// <param name="pRegistrations">The xml serialization of <c>Registrations</c>.</param>
		/// <remarks>
		/// <p>
		///		<c>NotCriticalDomainException</c>s may be indirectly thrown.
		///		<c>InvalidModelPropertiesException</c>s may be indirectly thrown.
		///		<c>NotAuthorizedException</c>s may be indirectly thrown in case of access control
		///		restrictions.
		///	</p>
		///	<p>
		///		If <paramref name="pRegistrations"/> is not in the expected format, nothing happens.
		///		The expected format is the following:
		///		<pre>
		///			<academic-schedule-registration-list>
		///				<period-list>
		///					<period>
		///						<course-list>
		///							<course id="{course identifier}" />
		///							....
		///							....
		///						</course-list>
		///					</period>
		///					....
		///					....
		///				</period-list>
		///			</academic-schedule-registration-list>
		///		</pre>
		///		The periods are assumed to be in ascending order from the <c>AcademicSchedule</c>'s
		///		<c>StartYear</c> and in steps of the <c>AcademicSchedule</c>'s <c>Plan</c>'s <c>Terms</c>.
		///		The <c>AcademicSchedule</c>'s <c>Registration</c>s are removed and added to match the supplied
		///		xml.
		/// </p>
		/// </remarks>
		/// <exception cref="T:AcademicPlanner.Domain.NotCriticalDomainException">
		/// <list type="bullet">
		///		<item>
		///		Thrown with error code <c>DomainErrorCode.C_ACADEMICSCHEDULE_TO_SAVE_REGISTRATIONS_OF_NOT_FOUND</c>
		///		if the <c>AcademicSchedule</c> with the given identifier does not exist.
		///		</item>
		///		<item>
		///		Thrown with error code <c>DomainErrorCode.C_INVALID_ACADEMICSCHEDULE_REGISTRATIONS_XML_FORMAT</c>
		///		if the <paramref name="pRegistrations"/> is not an xml string.
		///		</item>
		/// </list>
		/// </exception>
		/// <seealso cref="M:GetAcademicSchedule"/>
		/// <since>1.0</since>
		public void SaveAcademicScheduleRegistrations(int pScheduleId, string pRegistrations)
		{
			XmlDocument vXml;
			SaveAcademicScheduleRegistrationsTask vTask;
			var vSchedule = GetAcademicSchedule(pScheduleId);

			if (vSchedule == null)
				throw new NotCriticalDomainException(DomainErrorCode.C_ACADEMICSCHEDULE_TO_SAVE_REGISTRATIONS_OF_NOT_FOUND);
			vXml = new XmlDocument();
			try
			{
				vXml.LoadXml(pRegistrations);
				vTask = new SaveAcademicScheduleRegistrationsTask(vSchedule, vXml);
				vTask.Run();
			}
			catch (XmlException)
			{
				throw new NotCriticalDomainException(DomainErrorCode.C_INVALID_ACADEMICSCHEDULE_REGISTRATIONS_XML_FORMAT);
			}
		}


		class DeleteAcademicScheduleTask : Task
		{
			AcademicSchedule schedule;

			internal DeleteAcademicScheduleTask(AcademicSchedule pSchedule)
			{
				schedule = pSchedule;
			}

			protected override void Init()
			{
				// check authorization
				if (!DomainMaster.Auth.IsUserAuthorized(ACOperationName.DELETE_ACADEMICSCHEDULE, schedule))
					throw new NotAuthorizedException(DomainErrorCode.C_NOT_AUTHORIZED_DELETE_ACADEMICSCHEDULE);

				base.Init();
			}

			protected override void Perform()
			{
				schedule.Delete();
			}
		}

		class SaveAcademicScheduleRegistrationsTask : Task
		{
			AcademicSchedule schedule;
			XmlDocument registrations;

			internal SaveAcademicScheduleRegistrationsTask(AcademicSchedule pAcademicSchedule,
															XmlDocument pRegistrations)
			{
				schedule = pAcademicSchedule;
				registrations = pRegistrations;
			}

			protected override void Init()
			{
				// check authorization
				if (!DomainMaster.Auth.IsUserAuthorized(ACOperationName.UPDATE_ACADEMICSCHEDULE, schedule))
					throw new NotAuthorizedException(DomainErrorCode.C_NOT_AUTHORIZED_UPDATE_ACADEMICSCHEDULE);

				base.Init();
			}

			protected override void Perform()
			{
				var vOp1 = new UpdateAcademicScheduleRegistrationsOperation(schedule, registrations);
				vOp1.Launch();
			}
		}

		class SaveAcademicScheduleTask : Task
		{
			AcademicSchedule schedule;
			string name;

			internal SaveAcademicScheduleTask(AcademicSchedule pAcademicSchedule, string pName)
			{
				schedule = pAcademicSchedule;
				name = pName;
			}

			protected override void Init()
			{
				// check authorization
				if (!DomainMaster.Auth.IsUserAuthorized(ACOperationName.UPDATE_ACADEMICSCHEDULE, schedule))
					throw new NotAuthorizedException(DomainErrorCode.C_NOT_AUTHORIZED_UPDATE_ACADEMICSCHEDULE);

				base.Init();
			}

			protected override void Perform()
			{
				var vOp1 = new UpdateAcademicScheduleOperation(schedule, name, null, null, null);
				vOp1.Launch();
			}
		}

		class SaveNewAcademicScheduleTask : Task
		{
			AcademicPlan plan;
			string scheduleName;
			int? scheduleProgramId;
			protected int? startYear;
			protected int? startTermId;

			internal SaveNewAcademicScheduleTask(AcademicPlan pPlan, string pScheduleName, int? pScheduleProgramId,
													int? pStartYear, int? pStartTermId)
			{
				plan = pPlan;
				scheduleName = pScheduleName;
				scheduleProgramId = pScheduleProgramId;
				startYear = pStartYear;
				startTermId = pStartTermId;
			}

			protected override void Init()
			{
				// check authorization
				if (!DomainMaster.Auth.IsUserAuthorized(ACOperationName.CREATE_ACADEMICSCHEDULE, plan))
					throw new NotAuthorizedException(DomainErrorCode.C_NOT_AUTHORIZED_CREATE_ACADEMICSCHEDULE);

				base.Init();
			}

			protected override void Perform()
			{
				var vOp1 = new CreateAcademicScheduleOperation(plan, scheduleName, scheduleProgramId,
																startYear, startTermId);
				vOp1.Launch();
			}

			protected override void Succeeded()
			{
				// setup the new academic schedule for Access Control
				DomainMaster.Auth.RegisterIntoACSystem(plan.Schedules.ToList().Last(),
														DomainMaster.Planning
															.GetPlanner(SessionHelper.GetUserName()).User);
			}
		}

		class UpdateAcademicScheduleRegistrationsOperation : Operation
		{
			protected AcademicSchedule schedule;
			XmlDocument registrations;

			internal UpdateAcademicScheduleRegistrationsOperation(AcademicSchedule pSchedule,
																	XmlDocument pRegistrations)
			{
				schedule = pSchedule;
				registrations = pRegistrations;
			}

			protected override void Execute()
			{
				XmlNodeList vCourses;
				XmlNode vTemp;
				XmlNodeList vPeriods;
				string vRegsKey;
				var vCourseId = -1;
				var vTermIndex = -1;
				var vYear = schedule.StartYear - 1;
				var vTerms = schedule.Plan.Terms.ToList();

				vTemp = registrations.SelectSingleNode("/academic-schedule-registration-list");
				if (vTerms.Count > 0 && vTemp != null)
				{
					vTemp = vTemp.SelectSingleNode("child::period-list");
					if (vTemp != null)
					{
						var vRegistrations = schedule.Registrations.ToDictionary();
						var vRegsToKeep = new List<int>();

						vPeriods = vTemp.SelectNodes("child::period");
						for (var vI = 0; vI < vPeriods.Count; vI++)
						{
							vTermIndex = (vTermIndex + 1) % vTerms.Count;
							if ((vTermIndex % vTerms.Count) == 0)
								vYear++;

							vTemp = vPeriods[vI].SelectSingleNode("child::course-list");
							if (vTemp != null)
							{
								vCourses = vTemp.SelectNodes("child::course");

								for (var vJ = 0; vJ < vCourses.Count; vJ++)
								{
									vTemp = vCourses[vJ].SelectSingleNode("attribute::id");
									if (vTemp != null)
									{
										if (int.TryParse(vTemp.Value, out vCourseId))
										{
											vRegsKey = string.Format(RegistrationCollection.toDictionaryKeyFormat,
																		vYear, vTerms[vTermIndex].OrderNum,
																		vCourseId);

											if (!vRegistrations.ContainsKey(vRegsKey))
											{
												try
												{
													var vCourse = schedule.Plan.GetCourseWithId(vCourseId);

													if (vCourse == null)
														throw new InvalidModelPropertiesException("Registration", "Course",
																	InvalidModelPropertyErrorCode.NOT_PRESENT,
																	DomainErrorCode.C_NOT_PRESENT_REGISTRATION_COURSE);

													var vReg = DomainMaster.ModelsFountain.GetNewRegistration();
													
													vReg.Set(vYear, vTerms[vTermIndex], vCourse);
													schedule.AddRegistration(vReg);
													vReg.Persit();
													vRegsToKeep.Add(vReg.EntityId);
												}
												catch (InvalidModelPropertiesException vIx)
												{
													var vErrs = new InvalidModelPropertiesErrorCollection("AcademicSchedule");
													vErrs.AddInner("Registrations", vIx.Errors);
													throw new InvalidModelPropertiesException(DomainErrorCode.C_INVALID_ACADEMICSCHEDULE_REGISTRATIONS_PROPERTIES,
																								vErrs);
												}
											}
											else
											{
												var vRegErrs = schedule.ValidateRegistration(vRegistrations[vRegsKey].EntityId,
																vYear, vRegistrations[vRegsKey].Term,
																vRegistrations[vRegsKey].Course);

												if (vRegErrs.OuterErrors.Count > 0)
												{
													var vErrs = new InvalidModelPropertiesErrorCollection("AcademicSchedule");

													vErrs.AddInner("Registrations", vRegErrs);
													throw new InvalidModelPropertiesException(DomainErrorCode.C_INVALID_ACADEMICSCHEDULE_REGISTRATIONS_PROPERTIES, vErrs);
												}
												vRegsToKeep.Add(vRegistrations[vRegsKey].EntityId);
											}
										}
									}
								}

							}
						}

						foreach (var vRegistration in schedule.Registrations.ToList())
						{
							if (!vRegsToKeep.Contains(vRegistration.EntityId))
								schedule.RemoveRegistration(vRegistration);
						}
					}
				}
			}
		}

		class UpdateAcademicScheduleOperation : Operation
		{
			protected AcademicSchedule schedule;
			protected string name;
			protected Program program;
			protected AcademicTerm startTerm;
			protected int? startYear;

			internal UpdateAcademicScheduleOperation(AcademicSchedule pSchedule, string pName, Program pProgram,
														int? pStartYear, AcademicTerm pStartTerm)
			{
				schedule = pSchedule;
				name = pName;
				program = pProgram;
				startTerm = pStartTerm;
				startYear = pStartYear;
			}

			protected override void Execute()
			{
				DomainMaster.Auth.EnterPrivelegedAuthorizationMode();

				var vName = name ?? schedule.Name;
				var vProg = program ?? schedule.Program;
				var vStYear = startYear ?? schedule.StartYear;
				var vStTerm = startTerm ?? schedule.StartTerm;

				DomainMaster.Auth.ExitPrivelegedAuthorizationMode();

				schedule.Set(vName, vProg, vStYear, vStTerm);
			}
		}

		class CreateAcademicScheduleOperation : UpdateAcademicScheduleOperation
		{
			AcademicPlan plan;

			internal AcademicSchedule Schedule { get { return schedule; } }

			internal CreateAcademicScheduleOperation(AcademicPlan pPlan, string pName, int? pProgramId,
														int? pStartYear, int? pStartTermId)
				: base(null, pName, null, pStartYear, null)
			{
				plan = pPlan;
				if (pProgramId != null)
				{
					program = plan.Programs.WithId((int)pProgramId);
					if (program == null)
						throw new InvalidModelPropertiesException("AcademicSchedule", "Program",
							InvalidModelPropertyErrorCode.NOT_PRESENT,
							DomainErrorCode.C_NOT_PRESENT_ACADEMICSCHDULE_PROGRAM);
				}
				if (pStartTermId != null)
				{
					startTerm = plan.Terms.WithId((int)pStartTermId);
					if (startTerm == null)
						throw new InvalidModelPropertiesException("AcademicSchedule", "StartTerm",
							InvalidModelPropertyErrorCode.NOT_PRESENT,
							DomainErrorCode.C_NOT_PRESENT_ACADEMICSCHDULE_STARTTERM);
				}
			}

			protected override void Execute()
			{
				schedule = DomainMaster.ModelsFountain.GetNewAcademicSchedule();
				base.Execute();
				plan.AddSchedule(schedule);
				schedule.Persit();
			}
		}
	}
}
